import sys
import logging as log

class OpInner(object):
    BUY_CODE = 0
    SELL_CODE = 1

    BUY_STR = "buy"
    SELL_STR = "sell"
    UNKOWN_STR = "unknown"

    def __init__(self, op_code):
        self.op_code = op_code

    def __str__(self):
        if self.op_code == OpInner.BUY_CODE:
            return OpInner.BUY_STR
        elif self.op_code == OpInner.SELL_CODE:
            return OpInner.SELL_STR
        else:
            return OpInner.UNKOWN_STR

class Op(object):
    BUY = OpInner(OpInner.BUY_CODE)
    SELL = OpInner(OpInner.SELL_CODE)


class Transaction(object):
    def __init__(self, op, date, code, price, cost, quant, fee):
        self.op = op
        self.date = date
        self.code = code
        self.price = price
        self.cost = cost
        self.quant = quant
        self.fee = fee

    def __str__(self):
        market_value = self.price * self.quant
        profit = market_value - self.cost * self.quant
        return "op=[%s] dt=[%s] cd=[%s] prc=[%.2f] qnt=[%d] mv=[%.2f] prf=[%.2f] fee=[%.2f]" % (self.op,
                self.date, self.code, self.price, self.quant, market_value, profit, self.fee)


class Delegation(object):
    def __init__(self, op, date, code, price, quant, tunable=False):
        self.op = op
        self.date = date
        self.code = code
        self.price = price
        self.quant = quant
        self.tunable = tunable

    def __str__(self):
        return "op=[%s] dt=[%s] cd=[%s] prc=[%.2f] qnt=[%d] tb=[%s]" % (self.op,
                self.date, self.code, self.price, self.quant, self.tunable)


class Share(object):
    def __init__(self, code, cost, quant):
        self.code = code
        self.cost = cost
        self.quant = quant

    def __str__(self):
        return "cd=[%s] cst=[%.2f] qnt=[%d]" % (self.code, self.cost, self.quant)

    def inc(self, price, quant):
        self.cost = (self.cost * self.quant + price * quant) / (self.quant + quant)
        self.quant += quant
        return self.quant

    def dec(self, quant):
        self.quant -= quant
        return self.quant

    def market_value(self, price):
        return price * self.quant
        
    def loss_limit(self, atr):
        return self.cost - 2 * atr


class Account(object):
    commission_rate = 0.00025
    min_commission = 5
    stamp_tax_rate = 0.001
    transfer_base = 1000
    alot = 100

    def __init__(self, balance):
        self.balance = balance
        self.accum_fee = 0
        self.shares = {}
        self.transactions = []

    def __str__(self):
        position_str = "\n".join([str(share) for share in self.shares.values()])
        return "blc=[%.2f] ac_fee=[%.2f] pst=[%s]" % (self.balance, self.accum_fee, position_str)

    def bal_rate2quant(self, price, balance_rate):
        if balance_rate <= 0 or balance_rate > 1:
            log.error("invalid balance_rate: %.2f", balance_rate)
            return 0
        quant = int(self.balance * balance_rate / price / 100) * 100
        return quant

    def pos_rate2quant(self, code, share_rate):
        if code not in self.shares:
            log.info("invalid code: %s", code)
            return 0

        if share_rate <= 0 or share_rate > 1:
            log.error("invalid share_rate: %.2f", share_rate)
            return 0

        share = self.shares[code]
        # in case of precision problem
        quant = int(round(share.quant * share_rate))
        return quant

    def delegate(self, delegation):
        if delegation.op == Op.BUY:
            return self.buy(delegation.date, delegation.code, delegation.price,
                    delegation.quant, delegation.tunable)
        elif delegation.op == Op.SELL:
            return self.sell(delegation.date, delegation.code, delegation.price,
                    delegation.quant, delegation.tunable)
        else:
            log.error("unkown delegation op: %d", delegation.op)
            return None

    def buy(self, date, code, price, quant, tunable):
        if quant <= 0:
            log.error("invalid quant: %d", quant)
            return None

        fee = self.charge(code, price, quant, True)
        market_value = price * quant
        if market_value + fee > self.balance:
            if tunable:
                quant_tuned = quant - Account.alot
                log.info("no enough balance, tune quant from %d to %d", quant, quant_tuned)
                return self.buy(date, code, price, quant_tuned, False)
            else:
                log.info("no enough balance")
                return None

        self.balance -= market_value + fee
        self.accum_fee += fee
        if code in self.shares:
            self.shares[code].inc(price, quant)
        else:
            self.shares[code] = Share(code, price, quant)
        transaction = Transaction(Op.BUY, date, code, price, price, quant, fee)
        self.transactions.append(transaction)
        return transaction

    def sell(self, date, code, price, quant, tunable):
        if code not in self.shares:
            log.info("invalid code: %s", code)
            return None

        if quant <= 0 :
            log.info("invalid quant: %d", quant)
            return None

        share = self.shares[code]
        if share.quant < quant:
            log.info("no enough share to sell")
            if tunable:
                log.info("tune quant to %d", share.quant)
                return self.sell(date, code, price, share.quant, False)
            else:
                return None

        fee = self.charge(code, price, quant, False)
        market_value = price * quant
        if self.balance + market_value < fee:
            log.info("the balance and market_value cannot cover fee")
            return None

        amount = self.shares[code].dec(quant)
        if amount == 0:
            del self.shares[code]

        self.balance += market_value - fee
        self.accum_fee += fee

        transaction = Transaction(Op.SELL, date, code, price, share.cost, quant, fee)
        self.transactions.append(transaction)
        return transaction

    def charge(self, code, price, quant, is_buy):
        market_value = price * quant
        fee = max(market_value * Account.commission_rate, 5)
        if code[0] == "6":
            fee += (quant + Account.transfer_base) / Account.transfer_base
        if not is_buy:
            fee += market_value * Account.stamp_tax_rate
        return fee

    def get_quant(self, code):
        if code in self.shares:
            return self.shares[code].quant
        return 0

    def market_value(self, market_price):
        mv = 0.
        for share in self.shares.values():
            if share.code not in market_price:
                log.error("missing share code: %s", share.code)
                sys.exit(-1)
            mv += share.market_value(market_price[share.code])
        return mv

    def total_cost(self):
        cost = 0.
        for share in self.shares.values():
            cost += share.cost * share.quant
        return cost

    def market_profit(self, market_price):
        mv = self.market_value(market_price)
        cost = self.total_cost()
        return mv - cost

    def contains(self, code):
        return code in self.shares

    def get_share(self, code):
        if code not in self.shares:
            return None
        return self.shares[code]

    def get_codes(self):
        return self.shares.keys()

    def allot_shares(self, code, factor):
        assert(factor > 1e-6)
        if code in self.shares:
            share = self.shares[code]
            quant_old = share.quant
            share.quant = int(round(share.quant * (1 + factor)))
            exact_factor = float(share.quant) / quant_old
            share.cost /= exact_factor
